package org.pblue.tumblarity.task;

import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.collections.CollectionUtils;
import org.json.JSONObject;
import org.pblue.appengine.AppengineDataException;
import org.pblue.appengine.AppengineDataManager;
import org.pblue.appengine.Cache;
import org.pblue.appengine.CacheList;
import org.pblue.armyknife.Utils;
import org.pblue.tumblarity.data.bean.Event;
import org.pblue.tumblarity.data.bean.Follower;
import org.pblue.tumblarity.data.bean.Tumblelog;
import org.pblue.tumblarity.service.FetchAllService;

/**
 * This is a compare-queue task mapped to /task/compare. It has the following
 * parameters:
 * 
 * tumblelogId (required): the tumblelog id who's new followers or unfollowes
 * are to be identified.
 * 
 * @author mikolaj
 *
 */
public class Compare extends HttpServlet {

  private static final long serialVersionUID = 1L;
  
  public static final String JSON_KEY_URL = "url";
  public static final String JSON_KEY_THUMBNAIL = "thumbnail";
  public static final String JSON_KEY_NAME = "name";
  public static final String JSON_KEY_FOLLOWED = "followed";
  public static final String JSON_KEY_SIZE = "size";
  public static final String JSON_KEY_ACTION = "action";
  public static final String JSON_KEY_DATE = "date";
  public static final String JSON_KEY_ID = "id";
  
  public static final int SAVE_AFTER_RESET_RETRY_COUNT_TRESHOLD = 20;
  public static final int COMPARE_RETRY_TRESHOLD = 20;
  
  private static final Logger log = Logger.getLogger(Compare.class.getName());

  @Override
  protected void service(HttpServletRequest pRequest, HttpServletResponse pResponse) throws ServletException, IOException {
    long start = new Date().getTime();
    
    String tumblelogId = pRequest.getParameter(FetchAllService.PARAM_TUMBLELOG_ID);
    log.info("Identifying new follows and unfollows for Tumblelog " + tumblelogId + ".");

    AppengineDataManager dataManager = AppengineDataManager.open();

    try {
      // retrieve tumblelog from datastore (exit if it cannot be found)
      dataManager.begin();
      Tumblelog tumblelog = Tumblelog.get(tumblelogId,dataManager);
      if (tumblelog != null) {
        
        // exit if the tumblelog's fetch state isn't 'processing' (e.g. if the task was 
        // called manually)
        if (tumblelog.getFetchState() == Tumblelog.FETCH_STATE_PROCESSING) {

          CacheList<Follower> fetchedFollowers = tumblelog.getFetchedFollowers(dataManager);
          CacheList<Follower> followers = tumblelog.getFollowers(dataManager);
          
          if (fetchedFollowers != null && followers != null) {
            
            // invalidate cached lists of followers of tumblelog
            Cache.remove(Tumblelog.FOLLOWER_LIST_PREFIX + tumblelog.getId());
            Cache.remove(Tumblelog.EVENT_LIST_PREFIX + tumblelog.getId());

            // identify new followers by looping through the actually fetched followers
            // list and checking if each follower is contained by the real follower list of
            // this tumblelog. if a fetched follower not contained by the real followers 
            // list is found, then create a new follower event as a JSON string, add it to 
            // the tumbelog's events, create the new follower object and store it in a 
            // temporary list.
            Set<Follower> newFollowers = new HashSet<Follower>(CollectionUtils.subtract(fetchedFollowers, followers));
            Set<String> newFollowerIds = new HashSet<String>();
            
            for (Follower follower : newFollowers) {
              Map<String,String> eventMap = new HashMap<String,String>();
              eventMap.put(JSON_KEY_URL, follower.getUrl());
              eventMap.put(JSON_KEY_THUMBNAIL, follower.getThumbnail()); 
              eventMap.put(JSON_KEY_NAME, follower.getName());
              eventMap.put(JSON_KEY_FOLLOWED, (tumblelog.getTumblelog() != null && !tumblelog.getTumblelog().equals("") ? tumblelog.getTumblelog() : "you"));
              eventMap.put(JSON_KEY_SIZE, "" + tumblelog.getFetchedFollowers().size());
              eventMap.put(JSON_KEY_ACTION, "IS");
              eventMap.put(JSON_KEY_DATE, Utils.formatDate(tumblelog.getLastFetched(), "yyyy/MM/dd HH:mm:ss"));
              JSONObject json = new JSONObject(eventMap);

              String message = json.toString();
              Date date = tumblelog.getLastFetched();
              if (date == null) {
                date = new Date();
              }

              Event event = new Event(date, message, tumblelog);
              event.save(dataManager);

              tumblelog.getEventIds().add(event.getId());

              newFollowerIds.add(follower.getId());
            }

            // identify unfollows by looping through the real followers of this tumblelog
            // and checking if each follower is contained by the actually fetched follower's
            // list. if there's one found not to be among the actually fetched followers, 
            // then create an unfollow event as a JSON string, add it to the tumbelog's events,
            // and store the follower in a temporary unfollows list.
            Set<Follower> unfollows = new HashSet<Follower>(CollectionUtils.subtract(followers, fetchedFollowers));
            Set<String> unfollowIds = new HashSet<String>();
            for (Follower follower : unfollows) {
              Map<String,String> eventMap = new HashMap<String,String>();

              eventMap.put(JSON_KEY_URL, follower.getUrl());
              eventMap.put(JSON_KEY_THUMBNAIL, follower.getThumbnail()); 
              eventMap.put(JSON_KEY_NAME, follower.getName());
              eventMap.put(JSON_KEY_FOLLOWED, (tumblelog.getTumblelog() != null && !tumblelog.getTumblelog().equals("") ? tumblelog.getTumblelog() : "you"));
              eventMap.put(JSON_KEY_SIZE, "" + tumblelog.getFetchedFollowers().size());
              eventMap.put(JSON_KEY_ACTION, "IS NOT");
              eventMap.put(JSON_KEY_DATE, Utils.formatDate(tumblelog.getLastFetched(), "yyyy-MM-dd HH:mm:ss"));
              JSONObject json = new JSONObject(eventMap);

              String message = json.toString();
              Date date = tumblelog.getLastFetched();
              if (date == null) {
                date = new Date();
              }

              Event event = new Event(date, message, tumblelog);
              event.save(dataManager);

              tumblelog.getEventIds().add(event.getId());

              unfollowIds.add(follower.getId());
            }    

            // add new followers to the tumbelog's real followers list 
            tumblelog.getFollowerIds().addAll(newFollowerIds);

            // remove followers who's been identified unfollowing this tumblelog
            tumblelog.getFollowerIds().removeAll(unfollowIds);

            tumblelog.save(dataManager);

          }
          
          dataManager.commit();
          
          // clear the tumblelog's list of fetched pages and the list of actually 
          // fetched followers, and set its fetch state to 'idle'
          if (resetTumblelog(tumblelogId, 0, dataManager, false)) {
            log.info("Tumblelog " + tumblelogId + " processed in " + (new Date().getTime() - start) + "ms.");
          }

        } else {
          log.warning("Tumblelog " + tumblelogId + " is not being processed.");
        }

      } else {
        log.warning("Tumblelog " + tumblelogId + " not found.");
        resetTumblelog(tumblelogId, 0, dataManager, true);
      }

    } catch (Exception e) {
      log.log(Level.SEVERE, e.getMessage(), e);
      resetTumblelog(tumblelogId, 0, dataManager, true);
    } finally {
      dataManager.rollback();
      dataManager.close();
    }
    
  }

  /**
   * This method resets the passed in tumblelog by clearing the fetched pages list 
   * and the fetched followers list, and setting its fetch state property to 'idle'.
   * The it saves the tumblelog, and if cannot be saved then the method calls itself
   * by passing in an incremented save retry counter. If that save retry counter
   * reaches the save retry count limit, then an error message is logged and the
   * tumblelog stays in a 'processing' fetch state, which means it is never fetched
   * again.
   * 
   * @param pTumblelogId The tumblelog's id.
   * @param pSaveRetryCounter The save retry counter, initialized to 0 when the method
   * is first called, then incremented at every recursion.
   */
  private boolean resetTumblelog(String pTumblelogId, int pSaveRetryCounter, AppengineDataManager pDataManager, boolean pError) {
    try {
      if (!pDataManager.isOpen()) {
        pDataManager.begin();
      }
      Tumblelog tumblelog = Tumblelog.get(pTumblelogId, pDataManager);
      if (pError) {
        tumblelog.setFetchRetryCount(tumblelog.getFetchRetryCount() + 1);
        tumblelog.setFetchState(Tumblelog.FETCH_STATE_ERROR);
        log.warning("Tumblelog " + pTumblelogId + " has been reset and its fetch state set to ERROR.");
      } else {
        tumblelog.setFetchRetryCount(0);
        tumblelog.setLastFetched(new Date());
        tumblelog.setFetchState(Tumblelog.FETCH_STATE_IDLE);
      }
      tumblelog.save(pDataManager);
      pDataManager.commit();
      return true;
    } catch (AppengineDataException e) {
      log.log(Level.SEVERE, e.getMessage(), e);
      if (pSaveRetryCounter < SAVE_AFTER_RESET_RETRY_COUNT_TRESHOLD) {
        return resetTumblelog(pTumblelogId, pSaveRetryCounter + 1, pDataManager, true);
      } else {
        log.severe("Tumblelog " + pTumblelogId + " couldn't be reset, won't be fetched anymore.");
      }
    }
    
    return false;
  }
  
}
